Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 48cf5737c423ac69e88b36559d39711a85e6a8ec


Parents : 6b8c341
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-05T00:00:48-05:00

feat(relay_chat): improve room management by adding available rooms display and joining functionality

Changes
Diff

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index af44548f..9fa317f8 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -5157,7 +5157,8 @@ class ReticulumMeshChat:
{
"message": (
"RNode serial and Bluetooth are not available on this device. "
- "On Android, the app must include usbserial4a (see MeshChatX issue #6)."
+ "On Android, the app must include usbserial4a and jnius "
+ "(see MeshChatX issue #6)."
),
},
status=422,

diff --git a/meshchatx/src/backend/rrc/manager.py b/meshchatx/src/backend/rrc/manager.py
index 26cee162..de5aa8cc 100644
--- a/meshchatx/src/backend/rrc/manager.py
+++ b/meshchatx/src/backend/rrc/manager.py
@@ -482,9 +482,22 @@ class RRCHub:
def set_auto_list(self, enabled, save=True):
with self._lock:
self.auto_list = bool(enabled)
+ should_list = self.auto_list and self.welcomed
if save:
self.manager.save()
self.manager._notify_change(self)
+ if should_list:
+ self._request_room_list()
+
+ def _request_room_list(self):
+ try:
+ with self._lock:
+ self._silent_list_pending += 1
+ self.send_command("/list", room=None, record_local=False)
+ except Exception:
+ with self._lock:
+ if self._silent_list_pending > 0:
+ self._silent_list_pending -= 1
def set_auto_who(self, enabled, save=True):
with self._lock:
@@ -950,14 +963,7 @@ class RRCHub:
self._reconnect_attempts = 0
self.manager._on_welcome(self)
if self.auto_list:
- try:
- with self._lock:
- self._silent_list_pending += 1
- self.send_command("/list", room=None, record_local=False)
- except Exception:
- with self._lock:
- if self._silent_list_pending > 0:
- self._silent_list_pending -= 1
+ self._request_room_list()
def _apply_limits(self, limits):
if proto.L_MAX_NICK_BYTES in limits:

diff --git a/meshchatx/src/frontend/components/relay/RelayChatPage.vue b/meshchatx/src/frontend/components/relay/RelayChatPage.vue
index a84cedc3..6e9694b7 100644
--- a/meshchatx/src/frontend/components/relay/RelayChatPage.vue
+++ b/meshchatx/src/frontend/components/relay/RelayChatPage.vue
@@ -236,13 +236,46 @@
</span>
</li>
<li
- v-if="orderedRoomsFor(hub).length === 0"
+ v-if="orderedRoomsFor(hub).length === 0 && availableRoomsFor(hub).length === 0"
class="px-2.5 py-1 text-xs text-sem-fg-muted"
>
{{ $t("relay_chat.no_rooms") }}
</li>
</ul>
+ <div v-if="availableRoomsFor(hub).length > 0" class="space-y-0.5">
+ <div
+ class="px-2.5 pt-1 text-[10px] font-semibold uppercase tracking-wide text-sem-fg-muted"
+ >
+ {{ $t("relay_chat.available_rooms") }}
+ </div>
+ <ul class="space-y-0.5">
+ <li
+ v-for="availableRoom in availableRoomsFor(hub)"
+ :key="availableRoom.name"
+ :title="availableRoom.topic || ''"
+ class="flex items-center justify-between gap-2 rounded-lg px-2.5 py-1.5 text-sm cursor-pointer text-sem-fg-muted transition-colors hover:bg-sem-surface/60 dark:hover:bg-sem-surface/30"
+ @click="joinAvailableRoom(hub, availableRoom.name)"
+ >
+ <span class="flex min-w-0 items-center gap-1.5">
+ <MaterialDesignIcon
+ icon-name="pound"
+ class="size-3.5 shrink-0 opacity-40"
+ />
+ <span class="truncate">{{ availableRoom.name }}</span>
+ </span>
+ <button
+ type="button"
+ :class="btnIconSm"
+ :title="$t('relay_chat.join')"
+ @click.stop="joinAvailableRoom(hub, availableRoom.name)"
+ >
+ <MaterialDesignIcon icon-name="plus" class="size-3.5" />
+ </button>
+ </li>
+ </ul>
+ </div>
+
<form class="flex gap-1.5" @submit.prevent="joinRoom(hub)">
<input
v-model="joinRoomName"
@@ -1388,6 +1421,16 @@ export default {
}
return hub.known_rooms;
},
+ availableRoomsFor(hub) {
+ if (!hub || !hub.available_rooms || typeof hub.available_rooms !== "object") {
+ return [];
+ }
+ const known = new Set(this.orderedRoomsFor(hub));
+ return Object.entries(hub.available_rooms)
+ .filter(([name]) => !known.has(name))
+ .map(([name, topic]) => ({ name, topic }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+ },
onCollapsedHubClick(hub) {
const rooms = this.orderedRoomsFor(hub);
this.selectedHubHash = hub.hub_hash;
@@ -2032,13 +2075,23 @@ export default {
ToastUtils.warning(this.$t("relay_chat.room_required"));
return;
}
+ const joined = await this.joinRoomByName(hub, room);
+ if (joined) {
+ this.joinRoomName = "";
+ }
+ },
+ async joinAvailableRoom(hub, room) {
+ await this.joinRoomByName(hub, room);
+ },
+ async joinRoomByName(hub, room) {
try {
await window.api.post(`/api/v1/rrc/hubs/${hub.hub_hash}/rooms`, { room });
- this.joinRoomName = "";
ToastUtils.success(this.$t("relay_chat.joined_room"));
await this.fetchHubs();
+ return true;
} catch (e) {
ToastUtils.error(e.response?.data?.message || this.$t("relay_chat.action_failed"));
+ return false;
}
},
async leaveRoom() {

diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index 9bae63b7..e8bdb727 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -2904,6 +2904,8 @@
"dest_name": "Ziel-Aspekt (optional)",
"no_hubs": "Noch keine Hubs konfiguriert. Fügen Sie einen hinzu, um zu beginnen.",
"no_rooms": "Keine Räume beigetreten. Treten Sie einem Raum bei, um zu chatten.",
+ "available_rooms": "Verfügbare Räume",
+ "join": "Beitreten",
"no_room_selected": "Wählen Sie einen Raum, um Nachrichten anzuzeigen.",
"load_previous": "Frühere Nachrichten laden",
"rooms": "Räume",

diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index 8cd50789..55eb4a92 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -2263,6 +2263,8 @@
"dest_name": "Destination Aspect (optional)",
"no_hubs": "No hubs configured yet. Add one to get started.",
"no_rooms": "No rooms joined. Join a room to start chatting.",
+ "available_rooms": "Available Rooms",
+ "join": "Join",
"no_room_selected": "Select a room to view messages.",
"load_previous": "Load earlier messages",
"rooms": "Rooms",

diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index 8d794557..093e0cfd 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -2904,6 +2904,8 @@
"dest_name": "Aspecto de destino (opcional)",
"no_hubs": "Aun no hay hubs configurados. Anade uno para empezar.",
"no_rooms": "No te has unido a ninguna sala. Unete a una para chatear.",
+ "available_rooms": "Salas disponibles",
+ "join": "Unirse",
"no_room_selected": "Selecciona una sala para ver los mensajes.",
"load_previous": "Cargar mensajes anteriores",
"rooms": "Salas",

diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index 89581490..2ceafb79 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -2263,6 +2263,8 @@
"dest_name": "Destination Aspect (optional)",
"no_hubs": "No hubs configured yet. Add one to get started.",
"no_rooms": "No rooms joined. Join a room to start chatting.",
+ "available_rooms": "Available Rooms",
+ "join": "Join",
"no_room_selected": "Select a room to view messages.",
"load_previous": "Load earlier messages",
"rooms": "Rooms",

diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index 43c6d35b..b62fcaa9 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -2904,6 +2904,8 @@
"dest_name": "Aspect de destination (optionnel)",
"no_hubs": "Aucun hub configure. Ajoutez-en un pour commencer.",
"no_rooms": "Aucun salon rejoint. Rejoignez un salon pour discuter.",
+ "available_rooms": "Salons disponibles",
+ "join": "Rejoindre",
"no_room_selected": "Selectionnez un salon pour voir les messages.",
"load_previous": "Charger les messages precedents",
"rooms": "Salons",

diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index ef2c24e9..282656ff 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -2904,6 +2904,8 @@
"dest_name": "Aspetto di destinazione (opzionale)",
"no_hubs": "Nessun hub configurato. Aggiungine uno per iniziare.",
"no_rooms": "Nessuna stanza. Entra in una stanza per chattare.",
+ "available_rooms": "Stanze disponibili",
+ "join": "Entra",
"no_room_selected": "Seleziona una stanza per vedere i messaggi.",
"load_previous": "Carica messaggi precedenti",
"rooms": "Stanze",

diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index e4000d71..3ba22289 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -2904,6 +2904,8 @@
"dest_name": "Bestemmingsaspect (optioneel)",
"no_hubs": "Nog geen hubs geconfigureerd. Voeg er een toe om te beginnen.",
"no_rooms": "Geen kamers. Word lid van een kamer om te chatten.",
+ "available_rooms": "Beschikbare kamers",
+ "join": "Deelnemen",
"no_room_selected": "Selecteer een kamer om berichten te bekijken.",
"load_previous": "Eerdere berichten laden",
"rooms": "Kamers",

diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 01ff4f23..00b39dd9 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -2904,6 +2904,8 @@
"dest_name": "Аспект назначения (необязательно)",
"no_hubs": "Хабы ещё не настроены. Добавьте один, чтобы начать.",
"no_rooms": "Нет комнат. Присоединитесь к комнате, чтобы общаться.",
+ "available_rooms": "Доступные комнаты",
+ "join": "Присоединиться",
"no_room_selected": "Выберите комнату для просмотра сообщений.",
"load_previous": "Загрузить более ранние сообщения",
"rooms": "Комнаты",

diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index 9461de04..c9ec279b 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -2904,6 +2904,8 @@
"dest_name": "目标方面(可选)",
"no_hubs": "尚未配置中心。添加一个以开始。",
"no_rooms": "未加入任何房间。加入房间开始聊天。",
+ "available_rooms": "可用房间",
+ "join": "加入",
"no_room_selected": "选择一个房间以查看消息。",
"load_previous": "加载更早的消息",
"rooms": "房间",

diff --git a/tests/backend/test_rrc_protocol.py b/tests/backend/test_rrc_protocol.py
index 93b92a83..6062eb4f 100644
--- a/tests/backend/test_rrc_protocol.py
+++ b/tests/backend/test_rrc_protocol.py
@@ -251,6 +251,42 @@ def test_hub_notice_updates_room_list(tmp_path):
assert hub.available_rooms == {"lobby": "Main", "random": None}
+def test_welcome_sends_list_when_auto_list_enabled(tmp_path):
+ manager = make_manager(tmp_path)
+ hub = manager.add_hub(bytes(range(16)))
+ hub.auto_list = True
+ calls = []
+ hub.send_command = lambda text, room=None, record_local=True: calls.append(text)
+
+ hub._handle_welcome({})
+
+ assert calls == ["/list"]
+
+
+def test_set_auto_list_requests_list_immediately_when_already_connected(tmp_path):
+ """Enabling auto-list mid-session must not require a reconnect to take effect."""
+ manager = make_manager(tmp_path)
+ hub = manager.add_hub(bytes(range(16)))
+ hub.welcomed = True
+ calls = []
+ hub.send_command = lambda text, room=None, record_local=True: calls.append(text)
+
+ hub.set_auto_list(True)
+
+ assert calls == ["/list"]
+
+
+def test_set_auto_list_does_not_list_before_hub_is_connected(tmp_path):
+ manager = make_manager(tmp_path)
+ hub = manager.add_hub(bytes(range(16)))
+ calls = []
+ hub.send_command = lambda text, room=None, record_local=True: calls.append(text)
+
+ hub.set_auto_list(True)
+
+ assert calls == []
+
+
def test_history_is_persisted_and_reloaded(tmp_path):
manager = make_manager(tmp_path)
hub = manager.add_hub(bytes(range(16)))

diff --git a/tests/frontend/RelayChatPage.test.js b/tests/frontend/RelayChatPage.test.js
index ff2a67d5..15f9124f 100644
--- a/tests/frontend/RelayChatPage.test.js
+++ b/tests/frontend/RelayChatPage.test.js
@@ -130,6 +130,52 @@ describe("RelayChatPage.vue", () => {
expect(wrapper.vm.selectedHubHash).toBe(HUB_HASH);
});
+ it("shows hub rooms discovered via auto-list that have not been joined yet", async () => {
+ axiosMock.get.mockImplementation((url) => {
+ if (url === "/api/v1/rrc/hubs") {
+ return Promise.resolve({
+ data: { hubs: [makeHub({ available_rooms: { lobby: "Main", random: null } })] },
+ });
+ }
+ if (url === "/api/v1/rrc/servers") {
+ return Promise.resolve({ data: { hubs: [makeHostedHub()] } });
+ }
+ if (url === "/api/v1/announces") {
+ return Promise.resolve({ data: { announces: [makeAnnounce()] } });
+ }
+ return Promise.resolve({ data: {} });
+ });
+ const wrapper = mountPage();
+ await vi.waitFor(() => expect(wrapper.vm.hubs.length).toBe(1));
+
+ // "lobby" is already joined (known_rooms), so only "random" is unjoined.
+ expect(wrapper.vm.availableRoomsFor(wrapper.vm.hubs[0])).toEqual([{ name: "random", topic: null }]);
+ expect(wrapper.text()).toContain("random");
+ });
+
+ it("joins an unjoined available room via the API when clicked", async () => {
+ axiosMock.get.mockImplementation((url) => {
+ if (url === "/api/v1/rrc/hubs") {
+ return Promise.resolve({
+ data: { hubs: [makeHub({ available_rooms: { random: null } })] },
+ });
+ }
+ if (url === "/api/v1/rrc/servers") {
+ return Promise.resolve({ data: { hubs: [makeHostedHub()] } });
+ }
+ if (url === "/api/v1/announces") {
+ return Promise.resolve({ data: { announces: [makeAnnounce()] } });
+ }
+ return Promise.resolve({ data: {} });
+ });
+ const wrapper = mountPage();
+ await vi.waitFor(() => expect(wrapper.vm.hubs.length).toBe(1));
+
+ await wrapper.vm.joinAvailableRoom(wrapper.vm.hubs[0], "random");
+
+ expect(axiosMock.post).toHaveBeenCalledWith(`/api/v1/rrc/hubs/${HUB_HASH}/rooms`, { room: "random" });
+ });
+
it("loads messages and members when selecting a room", async () => {
const wrapper = mountPage();
await vi.waitFor(() => expect(wrapper.vm.hubs.length).toBe(1));

diff --git a/typings/jnius/__init__.pyi b/typings/jnius/__init__.pyi
new file mode 100644
index 00000000..f95df7b6
--- /dev/null
+++ b/typings/jnius/__init__.pyi
@@ -0,0 +1 @@
+# Optional Android dependency; stub for type checking only.


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────